로딩 중이에요... 🐣
04 요약정리 | ✅ 저자: 이유정(박사)
쇼핑몰 크롤링을 위한 정보 제공 및 디스코드 챗봇 구현 코드
🧭 프로젝트 흐름 요약
이 프로젝트는 무신사(Musinsa) 쇼핑몰 데이터를 크롤링하고, 디스코드 챗봇과 연동하여 사용자 요청에 따라 실시간으로 상품 정보를 제공합니다.
📌 전체 동작 흐름
-
사용자가 디스코드 채팅에서
신발추천
같은 명령어를 입력 -
bot.py
가 메시지를 인식하고scraper.py
를 호출하여 Musinsa 상품 데이터를 수집 -
크롤링된 데이터를 기반으로 추천 리스트, 최저가 상품, 상세 정보 등 분석
-
결과를 디스코드 Embed 메시지로 포맷팅하여 사용자에게 응답
✅ 디스코드 서버에서만 동작하며, 웹사이트 챗봇처럼 iframe 삽입은 불가능합니다.
📦 프로젝트 구조 개요
-
.env
: Discord 토큰 환경변수 저장 (예:DISCORD_TOKEN=your_token_here
) -
scraper.py
: Musinsa 상품 정보를 가져오는 API 크롤러 클래스 정의 -
bot.py
: 디스코드 챗봇 실행 코드 (아래 전체 코드 참고)
✅ scraper.py
import requests
from typing import List, Dict
class MusinsaAPI():
def __init__(self, keyword:str = "할인", page: int = 1, size: int = 60):
# API 엔드포인트
self.url = "https://api.musinsa.com/api2/dp/v1/plp/goods"
# 요청 파라미터 설정
self.params ={
"gf": "A",
"keyword": keyword,
"sortCode": "POPULAR",
"page": page,
"size": size,
"caller": "SEARCH",
}
self.headers = {
"User-Agent":(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
"AppleWebKit/537.36 (KHTML, like Gecko)"
"Chrome/138.0.0.0 Safari/537.36"
),
"Accept":"application/json, text/plain, */*",
}
# [{"keyword": keyword, "page": page}] # Get요청 수행
def fetch(self) -> List[Dict]:
response = requests.get(self.url, params=self.params, headers=self.headers)
response.raise_for_status()
data = response.json()
# 딕셔너리에서 안전하게 값을 꺼내오는 값을 조회하고 조건 검사하는 구문이다.
goods_list = data.get("data", {}).get("list", [])
if not goods_list:
return []
result: List[Dict] = []
for g in goods_list:
result.append({
"goodsNo": g.get("goodsNo"),
"name": g.get("goodsName", ""),
"brand": g.get("brandName", ""),
"normalPrice": f"{g.get("normalPrice", 0)}원",
"saleprice": f"{g.get("price", 0)}원",
"linkUrl": "https://api.musinsa.com/" + g.get("goodsLinkUrl", ""),
"imageUrl": g.get("imageUrl", ""),
})
return result
# # 실행 및 출력
api = MusinsaAPI()
items = api.fetch()
for item in items:
name = item.get("goodsName")
brand = item.get("brandName")
gender = item.get("displayGenderText", "")
price = item.get("price")
normal_price = item.get("normalPrice")
discount = item.get("saleRate", 0)
review_count = item.get("reviewCount", 0)
review_score = item.get("reviewScore", 0)
link = item.get("linkUrl")
image = item.get("imageUrl")
# 라벨 예: ["타임세일"]
labels = [label.get("title") for label in item.get("imageLabelList", [])]
print(f"🎁 {name} ({brand}) - {gender}")
print(f"💰 {normal_price}원 → {price}원 ({discount}% 할인)")
print(f"⭐ 리뷰: {review_score}/100 ({review_count}개)")
print(f"🏷️ 라벨: {', '.join(labels) if labels else '없음'}")
print(f"🔗 링크: {link}")
print(f"🖼️ 이미지: {image}")
print("-" * 60)
✅ bot.py
전체 코드
from dotenv import load_dotenv
import os
import re
import discord
from scraper import MusinsaAPI
# ─── 1. 환경 변수 로딩 ─────────────────────────────
load_dotenv()
# ─── 2. 디스코드 봇 클라이언트 설정 ───────────────
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
# ─── 3. Embed 메시지 생성 함수 ───────────────────
def build_message(item):
embed = discord.Embed(type="rich", title=item.get("name", "No name"), url=item.get("url", ""))
embed.set_thumbnail(url=item.get("image", ""))
embed.description = item.get("brand", "No brand")
embed.add_field(name="정가", value=item.get("originalPrice", "-"), inline=True)
embed.add_field(name="할인가", value=item.get("salePrice", "-"), inline=True)
return embed
# ─── 4. 채널별 추천 캐시 ──────────────────────────
last_recommendations: dict[int, dict] = {}
# ─── 5. 봇 실행 준비 이벤트 ───────────────────────
@client.event
async def on_ready():
print("▶ Running file:", __file__)
print(f"Logged in as {client.user}")
# ─── 6. 메시지 처리 이벤트 ───────────────────────
@client.event
async def on_message(message):
if message.author == client.user:
return
content = message.content.strip()
content_lower = content.lower()
channel_id = message.channel.id
print(f"[디버그] 메시지 수신: {content!r}")
# ─── 신발 추천 ─────────────────────
if ("신발추천" in content_lower or "신발 추천" in content_lower or content_lower == "신발"):
items = MusinsaAPI(keyword="신발", size=5).fetch() or []
last_recommendations[channel_id] = {
"keyword": "신발",
"page": 1,
"items": items
}
reply = "\n".join(f"{i+1}번. {item.get('name', '이름없음')} – {item.get('salePrice', '-')}" for i, item in enumerate(items))
await message.channel.send(f"👟 **신발 추천 TOP5**\n{reply}")
return
# ─── 반팔 추천 ─────────────────────
if ("반팔추천" in content_lower or "반팔 추천" in content_lower or "티셔츠추천" in content_lower or "티셔츠 추천" in content_lower):
items = MusinsaAPI(keyword="반팔", size=5).fetch() or []
last_recommendations[channel_id] = {
"keyword": "반팔",
"page": 1,
"items": items
}
reply = "\n".join(f"{i+1}번. {item.get('name', '이름없음')} – {item.get('salePrice', '-')}" for i, item in enumerate(items))
await message.channel.send(f"👕 **반팔 추천 TOP5**\n{reply}")
return
# ─── 상세보기 ─────────────────────
m = re.match(r"(\d+)번 상품 상세", content_lower)
if m and channel_id in last_recommendations:
idx = int(m.group(1)) - 1
items = last_recommendations[channel_id].get("items") or []
if 0 <= idx < len(items):
item = items[idx]
await message.channel.send(embed=build_message(item))
else:
await message.channel.send("❌ 해당 상품 번호를 찾을 수 없습니다.")
return
# ─── 찜하기 ─────────────────────
m = re.match(r"(\d+)번 상품 찜", content_lower)
if m and channel_id in last_recommendations:
await message.channel.send("💖 찜했어요! (가상 기능입니다)")
return
# ─── 가장 저렴한 상품 ─────────────────────
if "가장 저렴" in content_lower and channel_id in last_recommendations:
items = last_recommendations[channel_id].get("items") or []
try:
cheapest = min(items, key=lambda x: int(x.get("salePrice", "0").replace("원", "").replace(",", "")))
await message.channel.send(f"💸 가장 저렴한 상품:\n{cheapest.get('name', '이름없음')} – {cheapest.get('salePrice', '-')}")
except:
await message.channel.send("❌ 가격 정보를 비교할 수 없습니다.")
return
# ─── 다음 페이지 ─────────────────────
if "다음 페이지" in content_lower and channel_id in last_recommendations:
cache = last_recommendations[channel_id]
new_page = cache["page"] + 1
items = MusinsaAPI(keyword=cache["keyword"], page=new_page, size=5).fetch() or []
last_recommendations[channel_id] = {
"keyword": cache["keyword"],
"page": new_page,
"items": items
}
reply = "\n".join(f"{i+1}번. {item.get('name', '이름없음')} – {item.get('salePrice', '-')}" for i, item in enumerate(items))
await message.channel.send(f"📄 **{cache['keyword']} 추천 페이지 {new_page}**\n{reply}")
return
# ─── 기타 반응 ─────────────────────
if content_lower in ("안녕하세요", "안녕", "안녕하십니까"):
await message.channel.send(f"{message.author.display_name}님, 안녕하세요! 😊")
if content_lower.startswith("$hello"):
await message.channel.send("Hello!")
if content_lower.startswith("$hi") or content_lower == "hi":
await message.channel.send(f"hi, {message.author.display_name}! 🙂")
if content_lower.startswith("!타임세일"):
embeds = [build_message(i) for i in MusinsaAPI().fetch()[:10]]
await message.channel.send(embeds=embeds)
# ─── 7. 메인 진입점 ───────────────────────────────
if __name__ == "__main__":
token = os.getenv("DISCORD_TOKEN")
if not token:
raise RuntimeError("❌ DISCORD_TOKEN 환경변수가 설정되지 않았습니다.")
client.run(token)
🧪 디스코드 봇 사용 안내
봇 실행 후 콘솔에 다음과 같이 출력되면 정상 작동입니다:
▶ Running file: D:/경로/bot.py
Logged in as 봇이름#태그
1️⃣ 봇 초대 링크
아래 링크로 자신의 디스코드 서버에 봇을 초대해야 합니다:
👉 https://discord.com/oauth2/authorize?client_id=1390211542007677089&permissions=2&scope=bot
2️⃣ 디스코드 채팅에서 명령어 입력 예시
입력 내용 | 동작 설명 |
---|---|
신발추천 |
신발 관련 TOP5 상품 추천 목록 전송 |
2번 상품 상세 |
2번째 상품의 상세 Embed 메시지 전송 |
3번 상품 찜 |
"찜했어요" 메시지 전송 (가상 기능) |
가장 저렴 |
추천 목록 중 최저가 상품 출력 |
다음 페이지 |
다음 페이지 상품 리스트 출력 |
안녕하세요 , $hello , hi |
인사 메시지 응답 |
⚠️ 주의사항
-
.env
파일에DISCORD_TOKEN
설정이 필수입니다. -
scraper.py
에서 크롤링 로직이 정확해야 정보가 제대로 나타납니다. -
출력 값이
None
이라면 크롤링 대상 사이트의 구조 변경을 의심해보세요.
필요시 scraper.py
예제 코드도 제공해드릴 수 있습니다.